Loading Assets in Web
When building games for the web with the Strawberry Game Engine, asset loading works differently than on desktop or mobile platforms. Since the browser does not have direct access to a local file system, all game assets — such as textures, sounds, fonts, and data files — must be explicitly downloaded before they can be used. Strawberry provides a streamlined asset pipeline designed specifically for this environment, making it straightforward to fetch, cache, and read assets at runtime.
This document covers how to download assets ahead of time, how to access them once they are available, and best practices for structuring your loading workflow.
Why Asset Loading Is Different on the Web
On desktop platforms, your game can read files directly from disk using standard file I/O. The operating system provides a file system where assets sit alongside your executable, and you can open them at any time. The browser, however, runs your game inside a sandboxed environment with no access to the host file system. Everything your game needs must be transferred over HTTP from a server.
This has several important implications:
- Assets must be downloaded, not read from disk. Every texture, sound file, font, and data file must be fetched from a remote URL before your game code can use it.
- Downloads are asynchronous. Network requests take time and can fail, so your loading logic must handle asynchrony gracefully.
- Loading screens are essential. Unlike desktop where file reads are near-instant, web downloads can take seconds depending on file size and network conditions. You need to keep the player informed while assets are being fetched.
- Caching behavior depends on the browser. Once downloaded, assets may be cached by the browser, but you cannot rely on this across sessions. Your loading code should always assume assets need to be fetched.
Strawberry abstracts away much of this complexity by providing the GameLauncher class for web, which handles downloading assets and making them available through the IStorage interface.
AOT Downloading of Assets
AOT stands for Ahead-Of-Time, meaning the assets are downloaded before your game starts running. This is the most common and recommended approach for loading assets in a Strawberry web game. AOT downloading ensures that every resource your game needs is available in memory before the first frame is rendered, eliminating the risk of missing assets at runtime.
AOT downloading is especially well-suited for:
- Fonts — Text rendering requires font data to be available immediately; you cannot display a loading placeholder for missing glyphs.
- Core textures and sprite atlases — The visual foundation of your game should be ready before any scene is drawn.
- Configuration and data files — Game logic often depends on data files (level layouts, item definitions, etc.) that must be parsed before gameplay begins.
- Essential sound effects — Sounds that play during early gameplay should be loaded upfront to avoid silence or delays.
How AOT Downloading Works
The Strawberry.Web.GameLauncher class provides the AOTDownload method, which fetches a single asset from the server and stores it in memory. Each call to AOTDownload returns a Task that completes when the asset has finished downloading. Because each download is an independent task, you can download multiple assets in parallel using Task.WhenAll, which significantly reduces total loading time compared to downloading them sequentially.
Here is a complete example showing how to set up AOT downloading for a game called SpaceShooter:
using System.Runtime.Versioning;
using System.Threading.Tasks;
using SpaceShooter;
using Strawberry;
using Strawberry.Web;
[assembly: SupportedOSPlatform("browser")]
public static class Program
{
public static async Task Main(string[] args)
{
MyGameContext gameContext = new MyGameContext(240, 320);
Game game = new Game();
var l = new GameLauncher();
await Task.WhenAll(
l.AOTDownload("atlas.sbTex"),
l.AOTDownload("atlas.sprList"),
l.AOTDownload("bullet-laser.wav"),
l.AOTDownload("explosion-small.wav"),
l.AOTDownload("music01.ogg"),
l.AOTDownload("hesab.font"),
l.AOTDownload("elm.font"),
l.AOTDownload("powerup.wav")
);
game.Run(gameContext, l);
}
}
Breaking Down the Example
[assembly: SupportedOSPlatform("browser")]— This attribute tells the compiler that the assembly targets the browser platform. It is required for web-specific APIs to be accessible without compiler warnings.new GameLauncher()— Creates an instance of the web-specific game launcher. This launcher handles the browser's entry point, asset downloading, and game initialization.l.AOTDownload("filename")— Each call initiates an HTTP request to download the specified asset. The asset name must match the filename exactly as it exists on the server. The method returns aTaskthat resolves once the download completes.Task.WhenAll(...)— Combines all download tasks into a single task that completes only when every individual download has finished. This enables parallel downloading — the browser can fetch multiple files simultaneously, making the overall loading process much faster.await Task.WhenAll(...)— Awaits the completion of all downloads. The game will not proceed past this line until every asset is ready. This is what makes the loading "ahead of time."game.Run(gameContext, l)— Once all assets are downloaded, the game starts. The launcher ensures that the downloaded assets are accessible through the storage system.
Handling Loading Progress
Because Task.WhenAll blocks until all downloads complete, you can use it as a natural point to display or update a loading screen. If you want finer-grained progress reporting — for example, to show a progress bar — you can track individual download tasks instead:
var downloadTasks = new List<Task>
{
l.AOTDownload("atlas.sbTex"),
l.AOTDownload("atlas.sprList"),
l.AOTDownload("bullet-laser.wav"),
l.AOTDownload("explosion-small.wav"),
l.AOTDownload("music01.ogg"),
l.AOTDownload("hesab.font"),
l.AOTDownload("elm.font"),
l.AOTDownload("powerup.wav")
};
int total = downloadTasks.Count;
int completed = 0;
while (completed < total)
{
var finished = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finished);
completed++;
float progress = (float)completed / total;
// Update your loading screen with 'progress' (0.0 to 1.0)
}
This approach lets you report incremental progress to the player, which is especially useful for games with large asset bundles that may take several seconds to download.
Error Handling During Download
Network requests can fail for a variety of reasons: server errors, interrupted connections, or corrupted responses. It is good practice to wrap your download logic in try-catch blocks so that your game can respond gracefully to failures rather than crashing silently:
try
{
await Task.WhenAll(
l.AOTDownload("atlas.sbTex"),
l.AOTDownload("atlas.sprList"),
l.AOTDownload("music01.ogg")
);
}
catch (Exception ex)
{
// Log the error and inform the player
Console.WriteLine($"Asset download failed: {ex.Message}");
// Optionally retry or fall back to a minimal asset set
}
When a download fails, the AOTDownload task will fault, and the exception will propagate through Task.WhenAll. Catching it allows you to display an error message, retry the download, or attempt to continue with a reduced set of assets.
Using the Downloaded Assets
Once assets have been downloaded (whether via AOT downloading or another mechanism), they are available through the storage system provided by Strawberry.Core.IGameContext.Storage. The Storage property gives you access to an Strawberry.Misc.IStorage instance that abstracts over the browser's cached asset data.
There are two primary methods for reading downloaded assets:
Opening a Stream with Storage.Open
The Open method returns a standard C# Stream that you can use to read the asset data incrementally. This is useful when:
- You want to read only a portion of a large file without loading the entire thing into memory.
- You are working with a library or API that accepts a
Streamas input (such as audio decoders). - You need to parse a custom file format where you read headers, then selectively read sections of the data.
public class MyGameContext : StdGameContext
{
...
public override void OnInitialize(IGameLauncher launcher) {
...
// Open a stream for a music file — the OggReader reads from the stream
var music01 = SoundManager.CreateStream(new OggReader(Storage.Open("music01.ogg")));
...
}
...
}
In this example, Storage.Open("music01.ogg") provides a stream to the OGG audio file, which is then passed to the OggReader for decoding. The stream approach allows the audio decoder to read data in chunks rather than requiring the entire file to be loaded into a byte array first.
Reading an Entire File with Storage.ReadAllBytes
The ReadAllBytes method reads the entire contents of an asset into a byte[] array. This is the simplest way to load an asset and is ideal when:
- The file is relatively small and fits comfortably in memory.
- The consuming API expects a byte array rather than a stream.
- You need random access to the entire file contents.
public class MyGameContext : StdGameContext
{
...
public override void OnInitialize(IGameLauncher launcher) {
...
// Read an entire font file into memory as a byte array
Font = new Font(GraphicsContext, Storage.ReadAllBytes("elm.font"));
...
}
...
}
Here, Storage.ReadAllBytes("elm.font") loads the complete font file into a byte array, which is then passed to the Font constructor. This is straightforward and works well for files that are used in their entirety.
Choosing Between Open and ReadAllBytes
| Consideration | Storage.Open |
Storage.ReadAllBytes |
|---|---|---|
| Memory usage | Lower — reads data on demand | Higher — loads entire file into memory |
| Best for large files | Yes — avoids loading everything at once | No — may cause memory pressure |
| Best for small files | Works but adds unnecessary complexity | Simpler and just as efficient |
| Stream-based APIs | Required — passes a Stream directly |
Not suitable — must wrap bytes in a MemoryStream |
| Random access | Requires seeking within the stream | Built-in — byte array supports indexing |
| Simplicity | More code to manage the stream lifecycle | One-liner — immediately returns the data |
As a general rule, use ReadAllBytes for small to medium-sized assets (fonts, small textures, configuration files) and Open for large assets (music files, video data, large sprite atlases) or when working with stream-based APIs.
File Name Resolution
The input parameter for both Open and ReadAllBytes is always the exact file name that was used during the download. If you downloaded an asset as l.AOTDownload("atlas.sbTex"), then you access it with Storage.Open("atlas.sbTex") or Storage.ReadAllBytes("atlas.sbTex"). The name is case-sensitive and does not include any path prefix — it is simply the filename as it was registered during the download step.
If you attempt to access an asset that was not downloaded, the storage system will throw an exception indicating that the file was not found. Always ensure that every asset your game references has been included in the AOT download list.
Best Practices
Organize Your Asset List
Keep all AOT download calls in one place — typically the Main method or a dedicated static method. This makes it easy to audit which assets your game requires and avoids scattered download logic that is hard to maintain:
private static async Task DownloadAllAssets(GameLauncher launcher)
{
await Task.WhenAll(
// Textures
launcher.AOTDownload("atlas.sbTex"),
launcher.AOTDownload("atlas.sprList"),
// Audio
launcher.AOTDownload("bullet-laser.wav"),
launcher.AOTDownload("explosion-small.wav"),
launcher.AOTDownload("music01.ogg"),
launcher.AOTDownload("powerup.wav"),
// Fonts
launcher.AOTDownload("hesab.font"),
launcher.AOTDownload("elm.font")
);
}
Grouping assets by category (textures, audio, fonts, data) with comments makes the list self-documenting and easier to update as your game grows.
Minimize Total Asset Size
Every byte downloaded adds to your game's loading time. On the web, players have limited patience for loading screens, so it is important to keep your asset bundle as lean as possible:
- Optimize audio — Use efficient audio codecs (OGG for music, WAV for short sound effects) and trim silence from audio files.
- Avoid unused assets — Regularly audit your download list and remove assets that are no longer used in the game.
Separate Critical and Optional Assets
Not all assets need to be available before the game starts. Consider splitting your assets into two categories:
- Critical assets — Required for the game to function at all (core textures, fonts, essential sounds). Download these with AOT.
- Optional assets — Used later in the game (background music for later levels, cutscene assets, bonus content). These could potentially be loaded on demand after the game has started, if your architecture supports it.
By deferring non-essential assets, you reduce the initial loading time and get the player into the game faster.
Test on Slow Connections
Always test your loading flow on throttled network connections to simulate real-world conditions. What loads in under a second on a fast connection may take much longer for players on mobile networks or in regions with slower internet. A progress bar or animated loading indicator goes a long way toward keeping players engaged during long downloads.